DeviceIoControl 的一个用处

背景

最近一个月一直在做快速扫描磁盘文件的东西,主要是通过扫描NTFS格式的磁盘中的索引树(B-树,经过我查证,应该是B-树),可以得到整个磁盘文件的索引,然后通过遍历其索引树,即可得到目录下有哪些文件。这个需要对NTFS格式的磁盘的内部数据结构有一定的了解,这个可以参考我的另外一篇文章(这里还没有完成,待我完成时发布出来,当然网上也有很多资料)。

预期是希望能够比windows的API(FindFirstFileFindNextFile)效率要高,但是实际结果却不好,原因是磁盘IO太多了,因为首先找到根目录,然后找到根目录下的文件以及目录,然后再通过目录寻找其目录下的文件以及文件夹,所以需要不断的磁盘IO。

这里我使用的文件映射(CreateFileMapping好像不可以映射磁盘)、异步IO以及windows的完成端口(IOCP),最终的效果都不好。

然后经过老大的提醒,DeviceIOControl这个API也可以进行磁盘操作。查阅MSDN,这个API功能比较强大,功能比较多,发现其可以通过控制码直接和驱动交互,读取磁盘的File Entry。

因为它的效率比较高,最终采用的读取磁盘所有File Entry,而后自建索引树。

API详细介绍

API参数

1
2
3
4
5
6
7
8
9
10
BOOL WINAPI DeviceIoControl(
_In_ HANDLE hDevice,
_In_ DWORD dwIoControlCode,
_In_opt_ LPVOID lpInBuffer,
_In_ DWORD nInBufferSize,
_Out_opt_ LPVOID lpOutBuffer,
_In_ DWORD nOutBufferSize,
_Out_opt_ LPDWORD lpBytesReturned,
_Inout_opt_ LPOVERLAPPED lpOverlapped
);

参数一:设备句柄
参数二:控制码
参数三:输入缓冲区
参数四:输入缓冲区长度
参数五:输出缓冲区
参数六:输出缓冲区长度
参数七:返回的字节长度
参数八:一个OverLapped结构的指针

控制码

关于控制码主要有以下几个主题:

1
2
3
4
5
6
7
Communications Control Codes
Device Management Control Codes
Directory Management Control Codes
Disk Management Control Codes
File Management Control Codes
Power Management Control Codes
Volume Management Control Codes

控制码比较多,这里只介绍读取MFT的控制码,其他有兴趣可以参考MSDN。这个控制是File Management Control Codes中的 FSCTL_GET_NTFS_FILE_RECORD

这个控制在MSDN中的解释是Retrieves the first file record that is in use and is of a lesser than or equal ordinal value to the requested file reference number.说实话,我也没有完全理解这句话,暂且理解成通过file reference number获得file entry

控制码对应的API参数

再看该控制码对应的API参数结构:

1
2
3
4
5
6
7
8
BOOL DeviceIoControl( (HANDLE) hDevice, // handle to device
FSCTL_GET_NTFS_FILE_RECORD, // dwIoControlCode
(LPVOID) lpInBuffer, // input buffer
(DWORD) nInBufferSize, // size of input buffer
(LPVOID) lpOutBuffer, // output buffer
(DWORD) nOutBufferSize, // size of output buffer
(LPDWORD) lpBytesReturned, // number of bytes returned
(LPOVERLAPPED) lpOverlapped ); // OVERLAPPED structure

这里主要注意两个参数:
参数三:NTFS_FILE_RECORD_INPUT_BUFFER 结构指明你要读取的File Entry的编号,例如$MFT的编号为0,那么你就给该参数传入0,需要注意的是LARGER_INTEGER有64位,分为高32位和低32位。

1
2
3
typedef struct {
LARGE_INTEGER FileReferenceNumber;
} NTFS_FILE_RECORD_INPUT_BUFFER, *PNTFS_FILE_RECORD_INPUT_BUFFER;

参数五:NTFS_FILE_RECORD_OUTPUT_BUFFER 结构,用来保存读取的数据缓冲区。注意,FileRecordBuffer只占一个字节,如果数据没有读完,该API返回相应的提示。一般一个File Entry占2个扇区,一个扇区一般512字节,所以这里我一般将FileRecordBuffer改为占用1024字节的数组。

1
2
3
4
5
typedef struct {
LARGE_INTEGER FileReferenceNumber;
DWORD FileRecordLength;
BYTE FileRecordBuffer[1]; //可以改成FileRecordBuffer[1024]
} NTFS_FILE_RECORD_OUTPUT_BUFFER, *PNTFS_FILE_RECORD_OUTPUT_BUFFER;

相关代码

以上就是利用DeviceIoControl1读取File Entry时需要注意的地方,下面看下完整代码:
首先打开磁盘,注意CreateFile的各个参数的含义:

1
2
3
4
5
6
7
8
9
CString wStrVolum = L"\\\\.\\C:"; //以C盘为例
hVolume = CreateFile(
wStrVolum,
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
0,
OPEN_EXISTING,
0,
0);

然后读取File Entry:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
typedef struct {
LARGE_INTEGER FileReferenceNumber;
DWORD FileRecordLength;
BYTE FileRecordBuffer[1024];
} NTFS_FILE_RECORD_OUTPUT_BUFFER, *PNTFS_FILE_RECORD_OUTPUT_BUFFER;
int ReadMFT(FILE_RECORD_HEADER* mftRecord, ULONGLONG mftID)
{
if (mftRecord == NULL)
return -1;
NTFS_FILE_RECORD_INPUT_BUFFER nfrib;
NTFS_FILE_RECORD_OUTPUT_BUFFER2 nfrob;
if (hVolume != INVALID_HANDLE_VALUE)
{
nfrib.FileReferenceNumber.QuadPart = mftID;
DWORD bytesReturned = 0;
BOOL ret = DeviceIoControl(
hVolume,
FSCTL_GET_NTFS_FILE_RECORD,
&nfrib,
sizeof(NTFS_FILE_RECORD_INPUT_BUFFER),
&nfrob,
sizeof(NTFS_FILE_RECORD_OUTPUT_BUFFER2),
&bytesReturned,
NULL);
if (bytesReturned>0)
{
memcpy(mftRecord, nfrob.FileRecordBuffer, 1024);
return -1;
}
}
return 0;
}

效率

从我最终的测试结果来看,利用DeviceIoControl的控制码读取MFT的效率是非常快的。

  • 操作系统:windows 7 64 bit
  • 内存:8G
  • 测试对象:系统盘(C盘),大概有45万个文件

扫描系统盘所有File Entry,并且将读取的所有File Entry自建索引树,总共用时 2秒 不到。所以这个效率还是很高的。至少比ReadFile来读取MFT的效率要高。

如果本文对你有用,请留下你来过的痕迹,转载请注明出处[https://soygrow.github.io]!2016年8月4号于北京

热评文章